package com.liumapp.auth.zuul.gateway.filter;


import com.google.common.collect.Maps;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.security.crypto.codec.Base64;
import org.springframework.stereotype.Component;
import org.springframework.util.*;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Map;
import java.util.Objects;
import java.util.Timer;
import java.util.TimerTask;

/**
 * security access filter
 * @author sunshine
 */
@Component
public class AccessFilter extends ZuulFilter {

    private static Logger logger = LoggerFactory.getLogger(AccessFilter.class);
    private static final String ACCESS_TOKEN_KEY = "access_token" ;
    private static final String REFRESH_TOKEN_KEY = "refresh_token" ;
    private static final String CLIENT_CREDENTIALS = "client_credentials" ;
    private static final String PASSWORD = "password" ;
    private static final int REFRESH_TOKEN_SECONDS = 86400 - 10; //60*60*24 - 10 ;
    private RestOperations restTemplate = new RestTemplate();
    @Value("${security.oauth2.client.client-id}")
    private String clientId;
    @Value("${security.oauth2.client.client-secret}")
    private String clientSecret;
    @Value("${security.oauth2.client.access-token-uri}")
    private String accessTokenUrl;

    private static final Map<String, String> CACHE = Maps.newHashMap() ;

    public AccessFilter() {
        ((RestTemplate)this.restTemplate).setErrorHandler(new DefaultResponseErrorHandler() {
            public void handleError(ClientHttpResponse response) throws IOException {
                if (response.getRawStatusCode() != 400) {
                    super.handleError(response);
                }
            }
        });
    }

    /**
     *  pre : before
     *  routing : while routing
     *  post : after routing and error
     *  error : catch exception
     * @return String
     */
    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return FilterConstants.PRE_DECORATION_FILTER_ORDER + 2;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext context = RequestContext.getCurrentContext();
        String uri = context.getRequest().getRequestURI();
        logger.info("request uri : " + uri);
        boolean should = !(uri.endsWith("/login") || uri.contains("/oauth"));
        logger.info("shouldFilter = " + should);
        return should;
    }

    @Override
    public Object run() {
        try {
            RequestContext ctx = RequestContext.getCurrentContext();
            HttpServletRequest request = ctx.getRequest();
            synchronized (CACHE) {
//				Object accessToken = request.getParameter("accessToken");
                Object accessToken = CACHE.get(ACCESS_TOKEN_KEY);
                if(accessToken == null) {
//                  SysUser user = (SysUser) request.getSession().getAttribute(USER_LOGIN_SUCCESS_INFO_KEY);
                    Object user = null; //TODO:从登陆会话中取
                    if(user != null) {
                        String userName = "boss@gmail.com";
                        String password = "password";
                        logger.info("username : " + userName + "\tpassword : " + password);
                        accessToken = this.getOrRefreshAccessToken(PASSWORD, userName, password, null) ;
                    } else {
                        accessToken = this.getOrRefreshAccessToken(CLIENT_CREDENTIALS, null, null, null) ;
                    }
                }

                logger.info(String.format("%s request to %s with token %s", request.getMethod(), request.getRequestURL().toString(), accessToken));

                if(accessToken == null) {
                    logger.warn("access token is empty");
                    // or use ctx.setResponseBody(body) , that dependence on you
                    ctx.setSendZuulResponse(false);
                    ctx.setResponseStatusCode(401);
                    return null;
                }

                ctx.addZuulRequestHeader("Authorization", "Bearer " + accessToken);

                logger.info("this is access filter");
            }//
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private String getOrRefreshAccessToken(String grantType, String username, String password, String refreshToken) throws Exception {
        if(StringUtils.isBlank(grantType)) {
            throw new Exception("grant_type must not be null") ;
        }
        MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
        formData.add("grant_type",grantType);
        if(StringUtils.isNotBlank(username) && StringUtils.isNotBlank(password)) {
            formData.add("username", username);
            formData.add("password", password);
        }
        if(StringUtils.isNotBlank(refreshToken)) {
            formData.add(REFRESH_TOKEN_KEY, refreshToken);
        }
        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", this.getAuthorizationHeader(this.clientId, this.clientSecret));
        Map<String, Object> map = this.postForMap(this.accessTokenUrl, formData, headers);
        if (map.containsKey("error")) {
            logger.debug("check_token returned error: " + map.get("error"));
            throw new Exception("access token error");
        } else {
            Assert.state(map.containsKey(ACCESS_TOKEN_KEY), "Access token must be present in response from auth server");
            String accessToken = map.get(ACCESS_TOKEN_KEY).toString() ;
            int expires = (int)map.get("expires_in") ;
            logger.info(String.format("Get Access Token : %s , expires_in : %s", accessToken, expires));
            CACHE.put(ACCESS_TOKEN_KEY, accessToken) ;
            if(PASSWORD.equals(grantType)) {
                CACHE.put(REFRESH_TOKEN_KEY, Objects.toString(map.get(REFRESH_TOKEN_KEY))) ;
                this.refreshToken(expires);
                this.invalidToken(REFRESH_TOKEN_SECONDS);
            } else {
                this.invalidToken(expires);
            }
            return accessToken ;
        }
    }

    private void refreshToken(int seconds) {
        Timer timer= new Timer("refresh-token");
        TimerTask task = new TimerTask(){
            @Override
            public void run() {
                try {
                    if(CACHE.containsKey(REFRESH_TOKEN_KEY)){
                        getOrRefreshAccessToken("refresh_token", null, null, CACHE.get(REFRESH_TOKEN_KEY));
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
                logger.info("CACHE : " + CACHE);
            }
        };
        timer.schedule(task, seconds * 1000l);
    }

    private void invalidToken(int seconds) {
        Timer timer = new Timer("invalid-token") ;
        TimerTask task = new TimerTask() {
            @Override
            public void run() {
                CACHE.clear();
                logger.info("CACHE : " + CACHE);
            }
        };
        timer.schedule(task, seconds * 1000l);
    }

    private Map<String, Object> postForMap(String path, MultiValueMap<String, String> formData, HttpHeaders headers) {
        if (headers.getContentType() == null) {
            headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        }

        Map map = this.restTemplate.exchange(path, HttpMethod.POST, new HttpEntity(formData, headers), Map.class, new Object[0]).getBody();
        return map;
    }

    private String getAuthorizationHeader(String clientId, String clientSecret) {
        if (clientId == null || clientSecret == null) {
            logger.warn("Null Client ID or Client Secret detected. Endpoint that requires authentication will reject request with 401 error.");
        }

        String creds = String.format("%s:%s", clientId, clientSecret);

        try {
            return "Basic " + new String(Base64.encode(creds.getBytes("UTF-8")));
        } catch (UnsupportedEncodingException var5) {
            throw new IllegalStateException("Could not convert String");
        }
    }
}
